/*
 * Galaxium Messenger
 * Copyright (C) 2007 Ben Motmans <ben.motmans@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#define USE_SQLITE

using System;
using System.IO;
using System.Xml;
using System.Text;
using System.Collections.Generic;

using Galaxium.Core;
using Anculus.Core;

namespace Galaxium.Protocol
{
	//TODO: only create 1 instance for each log (re-use the same instance)
	public static class ConversationLogUtility
	{
		public static event EventHandler MaximumLogSizeChanged;
		
		public static event EventHandler LoggingEnabledChanged;
		
		private static bool _enableLogging;
		private static bool _enableEventLogging;
		
		private static string _logDirectory;
		
		private static int _maxLogSize;
		private static int _logChunkCount;
		
		private static IConfigurationSection _config;

		static ConversationLogUtility ()
		{
			_logDirectory = Path.Combine (Path.GetFullPath (CoreUtility.ConfigurationDirectory), "History");
			
			_config = Configuration.Logging.Section;
			
			_enableLogging = _config.GetBool (Configuration.Logging.EnableLogging.Name, Configuration.Logging.EnableLogging.Default);
			_enableEventLogging = _config.GetBool (Configuration.Logging.EnableEventLogging.Name, Configuration.Logging.EnableEventLogging.Default);
			
			int maxLogSize = _config.GetInt (Configuration.Logging.MaximumLogSize.Name, Configuration.Logging.MaximumLogSize.Default);
			
			if (maxLogSize < 262144)
				maxLogSize = 262144;
			
			MaximumLogSize = maxLogSize;
		}
		
		public static int MaximumLogSize
		{
			get {  return _maxLogSize; }
			set {
				//256kB = min, 2GB = max
				
				if (value == _maxLogSize)
					return;
				
				if (value < 262144) //256kB
					throw new ArgumentException ("MaximumLogSize must be >= 262144 (256kB)");

				_maxLogSize = value;
				_logChunkCount = GetLogChunkCount (_maxLogSize);
				
				_config.SetInt (Configuration.Logging.MaximumLogSize.Name, value);
				
				if (MaximumLogSizeChanged != null)
					MaximumLogSizeChanged (null, EventArgs.Empty);
			}
		}
		
		internal static int GetLogChunkCount (int bytes)
		{
			if (bytes < (1024 * 1024 * 4))
				return 4;
			else if (bytes < (1024 * 1024 * 64))
				return 8;
			else if (bytes < (1024 * 1024 * 128))
				return 16;
			else if (bytes < (1024 * 1024 * 512))
				return 32;
			else
				return 64;
		}
		
		public static bool EnableLogging
		{
			get { return _enableLogging; }
			set {
				if (_enableLogging != value) {
					_enableLogging = value;
					
					_config.SetBool (Configuration.Logging.EnableLogging.Name, value);
					
					if (LoggingEnabledChanged != null)
						LoggingEnabledChanged (null, EventArgs.Empty);
				}
			}
		}
		
		public static bool EnableEventLogging
		{
			get { return _enableEventLogging; }
			set {
				if (_enableEventLogging != value) {
					_enableEventLogging = value;
					_config.SetBool (Configuration.Logging.EnableEventLogging.Name, value);
				}
			}
		}
		
		public static int LogChunkCount
		{
			get { return _logChunkCount; }
		}
		
		public static int LogChunkSize
		{
			get { return _maxLogSize / _logChunkCount; }
		}

		public static string LogDirectory
		{
			get { return _logDirectory; }
		}
		
		public static bool ReadArchiveInfo (string directory, string logName, out int size, out int chunkCount)
		{
			string filename = Path.Combine (directory, "archives.xml");
			if (!File.Exists (filename)) {
				size = _maxLogSize;
				chunkCount = LogChunkCount;
				return false;
			}

			using (Stream stream = File.OpenRead (filename)) {
				using (XmlReader reader = new XmlTextReader (stream)) {
					while (reader.Read ()) {
						if (reader.Depth != 1 || reader.LocalName != "archive" || reader.NodeType != XmlNodeType.Element)
							continue;
						
						string name = reader.GetAttribute ("name");
						if (name != logName)
							continue;
						
						string sizeString = reader.GetAttribute ("size");
						string chunkCountString = reader.GetAttribute ("chunkCount");
						
						if (!int.TryParse (sizeString, out size) || !int.TryParse (chunkCountString, out chunkCount))
							return false;
						
						return true;
					}
				}
			}
			
			size = _maxLogSize;
			chunkCount = LogChunkCount;
			return false;
		}
		
		public static void WriteArchiveInfo (string directory, string logName, int size, int chunkCount)
		{
			string filename = Path.Combine (directory, "archives.xml");
			
			XmlDocument doc = new XmlDocument ();
			XmlElement rootElement = null;
			
			if (File.Exists (filename)) {
				try {
					doc.Load (filename);
					rootElement = doc.DocumentElement;
				} catch {}
			} else {
				rootElement = doc.CreateElement ("archives");
				doc.AppendChild (rootElement);
			}
			
			XmlElement archiveElement = null;
			foreach (XmlNode node in rootElement.SelectNodes ("/archive")) {
				if (node.NodeType != XmlNodeType.Element)
					continue;
				
				XmlElement element = node as XmlElement;
				
				string name = element.GetAttribute ("name");
				if (name != logName)
					continue;
				
				archiveElement = element;
				break;
			}
			
			if (archiveElement == null) {
				archiveElement = doc.CreateElement ("archive");
				archiveElement.SetAttribute ("name", logName);
				
				rootElement.AppendChild (archiveElement);
			}

			archiveElement.SetAttribute ("size", size.ToString ());
			archiveElement.SetAttribute ("chunkCount", chunkCount.ToString ());

			doc.Save (filename);
		}

		public static IConversationLog GetConversationLog (IConversation conversation)
		{
			ThrowUtility.ThrowIfNull ("conversation", conversation);
			
			string dir = GetLogDirectory (conversation.Session);
			
			string filename = null;
			if (conversation.IsPrivateConversation || conversation.IsChannelConversation)
			{
#if USE_SQLITE
				return new SQLiteConversationLog (dir, conversation.PrimaryContact.UniqueIdentifier, true);
#else
				return new IndexedConversationLog (dir, conversation.PrimaryContact.UniqueIdentifier, true);
#endif
			}
			else
				return GetGroupConversationLog (dir, conversation);
		}
		
		public static IConversationLog GetConversationLog (IContact contact)
		{
			ThrowUtility.ThrowIfNull ("contact", contact);

			string dir = GetLogDirectory (contact.Session);
			
#if USE_SQLITE
			return new SQLiteConversationLog (dir, contact.UniqueIdentifier, true);
#else
			return new IndexedConversationLog (dir, contact.UniqueIdentifier, true);
#endif
		}
		
		private static string GetLogDirectory (ISession session)
		{
			IAccount account = session.Account;
			
			IProtocolFactory fac = ProtocolUtility.GetProtocolFactory (account.Protocol);
			
			if (fac == null)
				return null;
			
			string dir = Path.Combine (Path.Combine (_logDirectory, fac.Protocol.Name), account.UniqueIdentifier);

			if (!Directory.Exists (dir))
				Directory.CreateDirectory (dir);
			
			return dir;
		}

		private static IConversationLog GetGroupConversationLog (string directory, IConversation conversation)
		{
			string subdir = Path.Combine (directory, "GroupChats");
			string lookupFile = Path.Combine (directory, "GroupChats.xml");
			
			if (!Directory.Exists (subdir))
				Directory.CreateDirectory (subdir);
			
			Stream stream = File.Open (lookupFile, FileMode.OpenOrCreate, FileAccess.ReadWrite);
			using (stream) {
				XmlReaderSettings settings = new XmlReaderSettings ();
				settings.ConformanceLevel = ConformanceLevel.Fragment;
				using (XmlReader reader = XmlReader.Create (stream, settings)) {
					while (reader.Read ()) {
						if (reader.Depth > 1 || reader.LocalName != "ConversationLog" || reader.NodeType != XmlNodeType.Element)
							continue;
						
						string countString = reader.GetAttribute ("count");
						int count = 0;
						if (int.TryParse (countString, out count))
							if (count != conversation.ContactCollection.Count)
								continue;
						
						if (IsConversationMatch (conversation, reader.ReadSubtree ()))
						{
							string logName = reader.GetAttribute ("logName");
							
#if USE_SQLITE
							return new SQLiteConversationLog (subdir, logName, true);
#else
							return new IndexedConversationLog (subdir, logName, true);
#endif
						}
					}
				}

				stream.Seek (stream.Length, SeekOrigin.Begin);
				
				using (XmlTextWriter writer = new XmlTextWriter (stream, Encoding.UTF8)) {
					writer.WriteStartElement ("ConversationLog");
					writer.WriteAttributeString ("logName", conversation.UniqueIdentifier.ToString ());
					writer.WriteAttributeString ("count", conversation.ContactCollection.Count.ToString ());
					
					foreach (IContact contact in conversation.ContactCollection) {
						writer.WriteStartElement ("Contact");
						writer.WriteAttributeString ("uid", contact.UniqueIdentifier);
						writer.WriteEndElement ();
					}
					
					writer.WriteEndElement ();
					writer.Flush ();
				}
				
#if USE_SQLITE
				return new SQLiteConversationLog (subdir, conversation.UniqueIdentifier.ToString (), true);
#else
				return new IndexedConversationLog (subdir, conversation.UniqueIdentifier.ToString (), true);
#endif
			}
		}
		
		private static bool IsConversationMatch (IConversation conversation, XmlReader reader)
		{
			bool match = true;

			while (reader.Read ()) {
				if (reader.Depth != 2 || reader.LocalName != "Contact" || reader.NodeType != XmlNodeType.Element)
					continue;
						
				string uid = reader.GetAttribute ("uid");
				
				bool found = false;
				foreach (IContact contact in conversation.ContactCollection) {
					if (contact.UniqueIdentifier == uid) {
						found = true;
						break;
					}
				}
				
				if (!found) {
					match = false;
					break;
				}
				
			}

			reader.Close ();
			return match;
		}
	}
}